package com.ppch.zerocodegenerator.controller;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.ppch.zerocodegenerator.ai.service_factory.AiCodeGenTypeRoutingServiceFactory;
import com.ppch.zerocodegenerator.ai.enums.CodeGenTypeEnum;
import com.ppch.zerocodegenerator.annotation.AuthCheck;
import com.ppch.zerocodegenerator.common.BaseResponse;
import com.ppch.zerocodegenerator.common.DeleteRequest;
import com.ppch.zerocodegenerator.common.ResultUtils;
import com.ppch.zerocodegenerator.exception.BusinessException;
import com.ppch.zerocodegenerator.exception.ErrorCode;
import com.ppch.zerocodegenerator.exception.ThrowUtils;
import com.ppch.zerocodegenerator.model.constant.AppConstant;
import com.ppch.zerocodegenerator.model.constant.UserConstant;
import com.ppch.zerocodegenerator.model.dto.app.*;
import com.ppch.zerocodegenerator.model.entity.App;
import com.ppch.zerocodegenerator.model.entity.User;
import com.ppch.zerocodegenerator.model.vo.AppVO;
import com.ppch.zerocodegenerator.ratelimiter.annotation.RateLimit;
import com.ppch.zerocodegenerator.ratelimiter.enums.RateLimitType;
import com.ppch.zerocodegenerator.service.AppService;
import com.ppch.zerocodegenerator.service.ProjectDownloadService;
import com.ppch.zerocodegenerator.service.UserService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.File;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

/**
 * 应用 控制层。
 *
 * @author 泡泡茶壶
 * @since 2025-10-5
 */
@RestController
@RequestMapping("/app")
@Slf4j
public class AppController {

    @Resource
    private AppService appService;

    @Resource
    private UserService userService;


    @Resource
    private ProjectDownloadService projectDownloadService;

    @Resource
    private AiCodeGenTypeRoutingServiceFactory aiCodeGenTypeRoutingServiceFactory;


    /**
     * 根据用户输入的初始Prompt创建应用
     *
     * @param appAddRequest 创建应用请求
     * @param request       请求
     * @return 应用 id
     */
    @PostMapping("/add")
    public BaseResponse<Long> addApp(@RequestBody AppAddRequest appAddRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(appAddRequest == null, ErrorCode.PARAMS_ERROR);
        // 参数校验
        String initPrompt = appAddRequest.getInitPrompt();
        ThrowUtils.throwIf(StrUtil.isBlank(initPrompt), ErrorCode.PARAMS_ERROR, "初始化 prompt 不能为空");
        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);
        // 构造入库对象
        App app = new App();
        BeanUtil.copyProperties(appAddRequest, app);
        app.setUserId(loginUser.getId());
        // 应用名称暂时为 initPrompt 前 12 位
        app.setAppName(initPrompt.substring(0, Math.min(initPrompt.length(), 12)));
        // TODO：使用 AI 来根据用户 Prompt 选择合适的代码生成模式
        CodeGenTypeEnum codeGenTypeEnum = aiCodeGenTypeRoutingServiceFactory.createAiCodeGenTypeRoutingService()
                .routeCodeGenType(initPrompt);
        String codeGenTypeEnumValue = codeGenTypeEnum.getValue();
        app.setCodeGenType(codeGenTypeEnumValue);
        //app.setCodeGenType(CodeGenTypeEnum.VUE_PROJECT.getValue()); //测试 VUE 工程项目生成
        log.info("AI智能选择代码生成模式：{}",codeGenTypeEnumValue);
        // 插入数据库
        boolean result = appService.save(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(app.getId());
    }

    /**
     * 应用聊天生成代码（流式 SSE）
     * 1.前端获取流式响应有可能会出现丢失空格字符、换行符的问题，例如后端响应：<div class，前端获取到的可能是<divclass
     *   解决方案：对响应再进行一层封装、或者对响应进行加密前端再进行解密
     *   采用方案：对响应再进行一层封装，将流式响应封装到Json中，{"d":"xxx"}
     * 2.在SSE中，当服务器关闭连接时会触发 OnClose 事件，前端通过监听 OnClose 事件来关闭连接的，
     *   但是前端是无法判断后端是正常流式输出了所有数据还是因为其它异常关闭了连接，所以
     *   后端要定义一个事件，当正常流式输出了所有数据后发送该事件，便于前端进行异常处理
     * @param appId   应用 ID
     * @param message 用户消息（用于接收用户的修改网站Prompt）
     * @param request 请求对象
     * @return 生成结果流
     */
    // 限流，每个用户每分钟最多请求5次
    @RateLimit(limitType = RateLimitType.USER,rate = 5,rateInterval = 60)
    @GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> chatToGenCode(@RequestParam Long appId,
                                      @RequestParam String message,
                                      HttpServletRequest request) {
        // 参数校验
        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID无效");
        ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空");
        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);
        // TODO：判断用户输入Prompt与示例应用Prompt是否完全一致，一致返回应用ID，不一致返回 0
        Long exampleApplicationAppId = appService.isWhetherConsistentPrompt(message);
        if (exampleApplicationAppId != 0) {
            // TODO：无需进入 AI 生成，直接返回已生成好的代码
            String aiMessage = appService.copyCodeFileAndHistory(exampleApplicationAppId, appId, loginUser);
            return Flux.create(sink -> {
                Map<String,String> chunkMap = Map.of("d",aiMessage);
                String jsonStr = JSONUtil.toJsonStr(chunkMap);
                ServerSentEvent<String> serverSentEvent = ServerSentEvent.<String>builder()
                        .data(jsonStr)
                        .build();
                sink.next(serverSentEvent);
                sink.complete();
            });
        }
        // 调用服务生成代码（流式）
        Flux<String> stringFlux = appService.chatToGenCode(appId, message, loginUser);
        return stringFlux
                //对响应再进行一层封装，将流式响应封装到Json中，{"d":"xxx"}，解决前端丢失空格字符、换行符的问题
                .map(chunk ->{
                    Map<String,String> chunkMap = Map.of("d",chunk);
                    String jsonStr = JSONUtil.toJsonStr(chunkMap);
                    return ServerSentEvent.<String>builder()
                            .data(jsonStr)
                            .build();
                })
                //定义一个done事件，当正常流式输出了所有数据后发送该事件，便于前端进行异常处理
                .concatWith(Mono.just(
                        ServerSentEvent.<String>builder()
                                .event("done")
                                .data("")
                                .build()
                        ));
    }

    /**
     * 应用部署
     * 1.支持多次重复部署，如果是第一次部署则生成 6 位 deployKey（大小写字母 + 数字）
     * 2.访问路径：http://localhost/deployKey，例如：http://localhost/34SOhR
     * @param appDeployRequest 部署请求（根据appID部署应用）
     * @param request          请求
     * @return 访问路径 URL
     */
    @PostMapping("/deploy")
    public BaseResponse<String> deployApp(@RequestBody AppDeployRequest appDeployRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(appDeployRequest == null, ErrorCode.PARAMS_ERROR);
        Long appId = appDeployRequest.getAppId();
        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
        // 获取当前登录用户
        User loginUser = userService.getLoginUser(request);
        // 调用服务部署应用
        String deployUrl = appService.deployApp(appId, loginUser);
        return ResultUtils.success(deployUrl);
    }




    /**
     * 下载应用代码压缩包
     *
     * @param appId    应用ID
     * @param request  请求
     * @param response 响应
     */
    @GetMapping("/download/{appId}")
    public void downloadAppCode(@PathVariable Long appId, HttpServletRequest request, HttpServletResponse response) {
        // 1. 基础校验
        ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID无效");
        // 2. 查询应用信息
        App app = appService.getById(appId);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
        // 3. 权限校验：只有应用创建者可以下载代码
        User loginUser = userService.getLoginUser(request);
        if (!app.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限下载该应用代码");
        }
        // 4. 构建应用代码目录路径（生成目录，非部署目录）
        String codeGenType = app.getCodeGenType();
        String sourceDirName = codeGenType + "_" + appId;
        // System.getProperty("user.dir") + /temp/code_output/codeGenType_appId
        String sourceDirPath = AppConstant.CODE_OUTPUT_ROOT_DIR + File.separator + sourceDirName;
        // 5. 检查代码目录是否存在
        File sourceDir = new File(sourceDirPath);
        ThrowUtils.throwIf(!sourceDir.exists() || !sourceDir.isDirectory(),
                ErrorCode.NOT_FOUND_ERROR, "应用代码不存在，请先生成代码");
        // 6. 生成下载文件名（不建议添加中文内容）
        String downloadFileName = String.valueOf(appId);
        // 7. 调用通用下载服务
        projectDownloadService.downloadProjectAsZip(sourceDirPath, downloadFileName, response);
    }



    /**
     * 更新应用信息（普通用户只能更新自己创建的应用的信息，暂时只支持更新应用名称，不支持更新应用封面）
     *
     * @param appUpdateRequest 更新请求
     * @param request          请求
     * @return 更新结果
     */
    @PostMapping("/update")
    public BaseResponse<Boolean> updateApp(@RequestBody AppUpdateRequest appUpdateRequest, HttpServletRequest request) {
        if (appUpdateRequest == null || appUpdateRequest.getId() == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User loginUser = userService.getLoginUser(request);
        long id = appUpdateRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人可更新
        if (!oldApp.getUserId().equals(loginUser.getId())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        App app = new App();
        app.setId(id);
        // 更新应用名称
        app.setAppName(appUpdateRequest.getAppName());
        // 设置编辑时间
        app.setEditTime(LocalDateTime.now());
        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }


    /**
     * 根据应用ID删除应用，同时会关联删除该应用的对话历史（用户只能删除自己的应用）
     *
     * @param deleteRequest 删除请求
     * @param request       请求
     * @return 删除结果
     */
    @PostMapping("/delete")
    public BaseResponse<Boolean> deleteApp(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User loginUser = userService.getLoginUser(request);
        long id = deleteRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        // 仅本人或管理员可删除
        if (!oldApp.getUserId().equals(loginUser.getId()) && !UserConstant.ADMIN_ROLE.equals(loginUser.getUserRole())) {
            throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
        }
        //TODO：删除应用，同时会关联删除该应用的对话历史
        boolean result = appService.removeById(id);
        return ResultUtils.success(result);
    }


    /**
     * 根据应用 id 获取应用详情（包含应用信息+创建该应用的用户信息）
     *
     * @param id 应用 id
     * @return 应用详情
     */
    @GetMapping("/get/vo")
    public BaseResponse<AppVO> getAppVOById(long id) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        App app = appService.getById(id);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
        // 获取封装类（包含用户信息）
        return ResultUtils.success(appService.getAppVO(app));
    }


    /**
     * 分页获取当前用户创建的应用列表（包含创建应用的用户信息）
     *
     * @param appQueryRequest 查询请求
     * @param request         请求
     * @return 应用列表
     */
    @PostMapping("/my/list/page/vo")
    public BaseResponse<Page<AppVO>> listMyAppVOByPage(@RequestBody AppQueryRequest appQueryRequest, HttpServletRequest request) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);
        User loginUser = userService.getLoginUser(request);
        // 限制每页最多 20 个
        long pageSize = appQueryRequest.getPageSize();
        ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");
        long pageNum = appQueryRequest.getPageNum();
        // 只查询当前用户的应用
        appQueryRequest.setUserId(loginUser.getId());
        // 根据查询条件查询对应的应用列表
        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);
        // 根据应用列表关联查询创建该应用的用户信息
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        // 返回最终数据：包含应用列表+用户信息
        appVOPage.setRecords(appVOList);
        return ResultUtils.success(appVOPage);
    }


    /**
     * 分页获取精选应用列表（现在只会查询优先级 priority 为 99 的应用）
     * 1.使用 Redis 缓存提高查询速度：@Cacheable该注解执行流程如下
     *   第一步：方法调用拦截：当 listGoodAppVOByPage 方法被调用时，Spring 的 AOP 拦截器会首先介入。
     *   第二步：解析注解中的值，如果不符合 condition 条件，整个缓存逻辑将被跳过，直接执行方法体。
     *   第三步：如果符合 condition 条件，执行缓存查找
     *          缓存命中（且能够被成功反序列化）：直接返回 Redis 中缓存的值，listGoodAppVOByPage 方法体完全不会被执行。
     *          缓存未命中（或反序列化失败）：继续执行 listGoodAppVOByPage 方法体，并将方法返回值 BaseResponse<Page<AppVO>> 写入到缓存中
     * @param appQueryRequest 查询请求
     * @return 精选应用列表
     */
    @PostMapping("/good/list/page/vo")
    @Cacheable(
            value = "good_app_page", // 缓存名称（命名空间）
            key = "T(com.ppch.zerocodegenerator.utils.CacheKeyUtils).generateKey(#appQueryRequest)", // 缓存key
            condition = "#appQueryRequest.pageNum <= 10" // 只缓存前 10 页的数据
    )
    public BaseResponse<Page<AppVO>> listGoodAppVOByPage(@RequestBody AppQueryRequest appQueryRequest) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);
        // 限制每页最多 20 个
        long pageSize = appQueryRequest.getPageSize();
        ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");
        long pageNum = appQueryRequest.getPageNum();
        // 只查询精选的应用
        appQueryRequest.setPriority(AppConstant.GOOD_APP_PRIORITY);
        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        // 分页查询
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);
        // 数据封装
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);
        return ResultUtils.success(appVOPage);
    }

    /**
     * 管理员删除应用（管理员可以删除任意应用）
     *
     * @param deleteRequest 删除请求
     * @return 删除结果
     */
    @PostMapping("/admin/delete")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> deleteAppByAdmin(@RequestBody DeleteRequest deleteRequest) {
        if (deleteRequest == null || deleteRequest.getId() <= 0) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        long id = deleteRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        boolean result = appService.removeById(id);
        return ResultUtils.success(result);
    }

    /**
     * 管理员更新应用
     *
     * @param appAdminUpdateRequest 更新请求
     * @return 更新结果
     */
    @PostMapping("/admin/update")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Boolean> updateAppByAdmin(@RequestBody AppAdminUpdateRequest appAdminUpdateRequest) {
        if (appAdminUpdateRequest == null || appAdminUpdateRequest.getId() == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        long id = appAdminUpdateRequest.getId();
        // 判断是否存在
        App oldApp = appService.getById(id);
        ThrowUtils.throwIf(oldApp == null, ErrorCode.NOT_FOUND_ERROR);
        App app = new App();
        BeanUtil.copyProperties(appAdminUpdateRequest, app);
        // 设置编辑时间
        app.setEditTime(LocalDateTime.now());
        boolean result = appService.updateById(app);
        ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
        return ResultUtils.success(true);
    }

    /**
     * 管理员分页获取应用列表
     *
     * @param appQueryRequest 查询请求
     * @return 应用列表
     */
    @PostMapping("/admin/list/page/vo")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<Page<AppVO>> listAppVOByPageByAdmin(@RequestBody AppQueryRequest appQueryRequest) {
        ThrowUtils.throwIf(appQueryRequest == null, ErrorCode.PARAMS_ERROR);
        long pageNum = appQueryRequest.getPageNum();
        long pageSize = appQueryRequest.getPageSize();
        QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
        Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);
        // 数据封装
        Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
        List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
        appVOPage.setRecords(appVOList);
        return ResultUtils.success(appVOPage);
    }


    /**
     * 管理员根据 id 获取应用详情
     *
     * @param id 应用 id
     * @return 应用详情
     */
    @GetMapping("/admin/get/vo")
    @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
    public BaseResponse<AppVO> getAppVOByIdByAdmin(long id) {
        ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);
        // 查询数据库
        App app = appService.getById(id);
        ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR);
        // 获取封装类
        return ResultUtils.success(appService.getAppVO(app));
    }



}
